Skip to content

feat(notifications): implement Notifications page (#396) - #434

Merged
0xdevcollins merged 2 commits into
boundlessfi:mainfrom
JamesVictor-O:feature/notifications-page-396
Mar 3, 2026
Merged

feat(notifications): implement Notifications page (#396)#434
0xdevcollins merged 2 commits into
boundlessfi:mainfrom
JamesVictor-O:feature/notifications-page-396

Conversation

@JamesVictor-O

@JamesVictor-O JamesVictor-O commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves #396Implementation of "Notifications" Page (app/me/notifications)

This PR replaces the /me/notifications coming-soon redirect with a fully functional, real-time notification center.

What's implemented:

  • Grouped notification feed — Notifications are organized into three sections with clear visual separators:
    • New — unread notifications
    • Earlier — read notifications from the past 7 days
    • Archived — older read notifications
  • BoundlessSheet detail view — Clicking any notification opens a BoundlessSheet from @/components/sheet/ displaying the full notification content, metadata (organization, hackathon, project, amounts, roles, etc.), and timestamps
  • Mark All as Read — Prominent button with smooth Motion animations for fading out unread indicators; button gracefully exits via AnimatePresence when no unread notifications remain
  • Real-time pollinguseNotificationPolling() runs at 30s intervals to keep the feed up-to-date without page refresh
  • Global sidebar badge sync — Created a Zustand store (notification-store.ts) that keeps the sidebar unreadCount badge in sync with the notification feed state. The AppSidebar now reads from this store instead of displaying a hardcoded '5' badge
  • Graceful empty states — Each section renders a brief, on-brand message when empty
  • Full accessibility — Notification items use semantic <button> elements with descriptive aria-label attributes, section headers use aria-label, and the BoundlessSheet supports keyboard dismissal (Escape) and focus trapping

Files changed:

File Change
app/me/notifications/page.tsx Replaced redirect with full notifications page
app/me/notifications/components/NotificationSection.tsx New — section component with grouping and animations
app/me/notifications/components/NotificationFeedItem.tsx New — individual notification item with unread indicator
app/me/notifications/components/NotificationDetailSheet.tsx New — BoundlessSheet with full notification details
lib/stores/notification-store.ts New — Zustand store for global unread count sync
components/app-sidebar.tsx Updated to use live unread count from store

Test plan

  • Confirm useNotifications() correctly loads and renders the notification feed on page mount
  • Verify useNotificationPolling() is active and new notifications appear without page refresh
  • Confirm notifications are correctly grouped into "New", "Earlier", and "Archived" sections
  • Test clicking a notification — confirm BoundlessSheet opens with correct full content
  • Verify opening a notification via the sheet marks it as read and updates the unread indicator
  • Confirm sidebar notification badge updates immediately after read state changes
  • Test "Mark All as Read" — confirm unread indicators fade out smoothly and button disappears
  • Verify the sheet opens/closes smoothly with animation, focus trapping, and Escape key dismissal
  • Test all three empty states (no new, no earlier, no archived) render gracefully
  • Confirm the page is fully accessible via screen reader

Summary by CodeRabbit

  • New Features
    • Notification center with feed displaying grouped notifications (New, Earlier, Archived)
    • Detailed notification view with metadata and formatted values
    • Mark individual or all notifications as read
    • Real-time notification polling and updates
    • Unread notification counter in sidebar
    • Pagination support for notification feed
    • Loading states, error handling, and empty state messages

…detail sheet, and live sidebar badge (#396)

Replace the coming-soon redirect with a full notifications center at /me/notifications.
Notifications are grouped into New/Earlier/Archived sections, clickable items open
a BoundlessSheet with full details, Mark All as Read uses motion animations, and the
sidebar badge reflects the live unread count via a Zustand store.

Made-with: Cursor
@vercel

vercel Bot commented Mar 3, 2026

Copy link
Copy Markdown

@JamesVictor-O is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a complete notifications center UI featuring four new React components for displaying notifications with real-time updates, a Zustand store for managing global unread counts, and integration with the sidebar badge. The main page replaces a redirect with a full client-side application that fetches, groups, and manages notifications with polling, mark-as-read functionality, and detail view support.

Changes

Cohort / File(s) Summary
Notification UI Components
app/me/notifications/components/NotificationDetailSheet.tsx, app/me/notifications/components/NotificationFeedItem.tsx, app/me/notifications/components/NotificationSection.tsx
Three new components rendering notification feed items and detail modal. DetailSheet displays notification content with metadata derived from notification.data; FeedItem shows icon, title, time, and unread indicator with animation; Section groups notifications by title with dynamic icons and empty states.
Notifications Page
app/me/notifications/page.tsx
Replaced redirect with full client-side page. Implements useNotifications, useNotificationPolling, and useNotificationStore hooks to fetch, poll, and manage state. Adds grouping logic (New/Earlier/Archived), mark-individual/mark-all-as-read actions with optimistic updates, pagination, loading/error states, and integrates NotificationDetailSheet and three NotificationSection instances.
Global State Management
lib/stores/notification-store.ts
New Zustand store exporting useNotificationStore hook with unreadCount state and three mutators: setUnreadCount, decrementUnreadCount, and clearUnreadCount. Provides reactive read/write access to unread count across the app.
Navigation Integration
components/app-sidebar.tsx
Added unreadNotifications field to navigation counts type and wired useNotificationStore hook to supply unreadCount. Notifications badge now conditionally renders dynamic unread count instead of static value; memoization updated to depend on both counts and unreadNotifications.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page as NotificationsPage
    participant Store as NotificationStore
    participant Hooks as Custom Hooks
    participant Sidebar as AppSidebar

    User->>Page: Opens notifications page
    Page->>Hooks: useNotifications() + useNotificationPolling()
    Hooks-->>Page: Initial notifications list + polling started
    Page->>Page: Group notifications into New/Earlier/Archived
    Page-->>User: Render NotificationSection components
    
    User->>Page: Click notification item
    Page->>Page: Open NotificationDetailSheet
    Page->>Hooks: Mark notification as read
    Hooks->>Store: setUnreadCount(newCount) / decrementUnreadCount()
    Store-->>Page: unreadCount updated
    Page->>Sidebar: unreadCount synced (via useNotificationStore)
    Sidebar-->>User: Badge count updated in real-time
    
    User->>Page: Click "Mark All as Read"
    Page->>Hooks: markAllAsRead()
    Hooks->>Store: clearUnreadCount()
    Store-->>Sidebar: unreadCount = 0
    Sidebar-->>User: Badge disappears
    Page-->>User: Unread indicators fade out
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • 0xdevcollins

Poem

🐰✨ A notification feast so grand,
Bells that chime at my command,
Details dance in a sheet so fair,
Unread counts float through the air,
Mark them read without a care! 📬

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing a notifications page. It is concise, specific, and directly related to the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all primary coding objectives from issue #396: grouped notification sections, BoundlessSheet detail view, real-time polling, global unread count sync via Zustand store, proper animations, and accessibility features.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #396 requirements: notification page components, store implementation, and sidebar integration. No unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/me/notifications/page.tsx (1)

93-95: Silent error swallowing may hide issues.

The .catch(() => {}) silently ignores errors when marking a notification as read. While this prevents UX disruption, consider at least logging the error for debugging:

♻️ Optional: log the error
       if (!notification.read) {
-        markNotificationAsRead([notification.id]).catch(() => {});
+        markNotificationAsRead([notification.id]).catch(err => {
+          console.error('Failed to mark notification as read:', err);
+        });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/me/notifications/page.tsx` around lines 93 - 95, The current call to
markNotificationAsRead([notification.id]) swallows errors with .catch(() => {});
change this to catch and surface the error (e.g., .catch(err =>
console.error(...)) or use the app logger) so failures to mark notifications are
logged; locate the call that checks notification.read and update the promise
error handler for markNotificationAsRead to log the error and, optionally, show
a non-blocking user notification.
app/me/notifications/components/NotificationFeedItem.tsx (1)

73-78: Consider edge case: amount === 0 won't render the badge.

The truthiness check notification.data.amount && will not render the badge when amount is 0. If zero-amount notifications are valid and should display "$0", consider using notification.data.amount !== undefined && notification.data.amount !== null instead.

If zero amounts are not expected or should be hidden, the current implementation is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/me/notifications/components/NotificationFeedItem.tsx` around lines 73 -
78, The conditional rendering in NotificationFeedItem currently uses the truthy
check notification.data.amount && which skips amount === 0; update the condition
to explicitly test for presence (e.g., notification.data.amount !== undefined &&
notification.data.amount !== null or notification.data.amount != null) so a zero
value will render the badge when zero-amount notifications should display; keep
the existing badge markup inside the same block using the updated condition.
app/me/notifications/components/NotificationDetailSheet.tsx (1)

127-128: Type assertion on amount assumes numeric type.

The cast (value as number).toLocaleString() will fail at runtime if amount is not a number (e.g., if it's a string from the API). Consider a safer approach:

🛡️ Safer amount formatting
 {key === 'amount'
-  ? `$${(value as number).toLocaleString()}`
+  ? `$${Number(value).toLocaleString()}`
   : key === 'transactionHash'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/me/notifications/components/NotificationDetailSheet.tsx` around lines 127
- 128, In NotificationDetailSheet, the code path that formats amount (the
conditional when key === 'amount') unsafely casts value with (value as
number).toLocaleString(); change this to validate and coerce the value safely:
check typeof value === 'number' first, else attempt to parse a numeric string
(e.g., Number(value) or parseFloat) and guard isFinite before calling
toLocaleString; if parsing fails, render a sensible fallback (original value or
'--'). Update the conditional in NotificationDetailSheet where key === 'amount'
to use this safe coercion/guarding logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/me/notifications/components/NotificationDetailSheet.tsx`:
- Around line 127-128: In NotificationDetailSheet, the code path that formats
amount (the conditional when key === 'amount') unsafely casts value with (value
as number).toLocaleString(); change this to validate and coerce the value
safely: check typeof value === 'number' first, else attempt to parse a numeric
string (e.g., Number(value) or parseFloat) and guard isFinite before calling
toLocaleString; if parsing fails, render a sensible fallback (original value or
'--'). Update the conditional in NotificationDetailSheet where key === 'amount'
to use this safe coercion/guarding logic.

In `@app/me/notifications/components/NotificationFeedItem.tsx`:
- Around line 73-78: The conditional rendering in NotificationFeedItem currently
uses the truthy check notification.data.amount && which skips amount === 0;
update the condition to explicitly test for presence (e.g.,
notification.data.amount !== undefined && notification.data.amount !== null or
notification.data.amount != null) so a zero value will render the badge when
zero-amount notifications should display; keep the existing badge markup inside
the same block using the updated condition.

In `@app/me/notifications/page.tsx`:
- Around line 93-95: The current call to
markNotificationAsRead([notification.id]) swallows errors with .catch(() => {});
change this to catch and surface the error (e.g., .catch(err =>
console.error(...)) or use the app logger) so failures to mark notifications are
logged; locate the call that checks notification.read and update the promise
error handler for markNotificationAsRead to log the error and, optionally, show
a non-blocking user notification.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9d26bd3 and c23a214.

📒 Files selected for processing (6)
  • app/me/notifications/components/NotificationDetailSheet.tsx
  • app/me/notifications/components/NotificationFeedItem.tsx
  • app/me/notifications/components/NotificationSection.tsx
  • app/me/notifications/page.tsx
  • components/app-sidebar.tsx
  • lib/stores/notification-store.ts

@0xdevcollins
0xdevcollins merged commit 6d92305 into boundlessfi:main Mar 3, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementation of "Notifications" Page

2 participants